<?php
/*
 * Copyright (c) 2008 Sergey Bondari, bondari@bondari.net
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 * - The name of the author may not be used to endorse or promote products
 *   derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

namespace Mosaic\System;

/**
 * Component broker
 *
 * The component broker is responsible for the management of component instances. 
 * Stateful component instances are singleton in the context scope, stateless component 
 * instances are singleton in the worker process scope. The component broker's job is to 
 * deliver a reference of a correct component instance based on the current worker context. 
 * This delivery operation may include loading of a relevant component class code and the 
 * preparation of some resources, of which a component depends on and after instantiation 
 * of the component class instance. After this, a component class instance is placed in the 
 * component broker pool for future usage. Components are removed from this pool during the 
 * context release operation.
 *
 * @package System
 * @subpackage Node
 */
class ComponentBroker {
	/**
	 * Components pool
	 * @var array
	 */
	private $components;


	/**
	 * Kernel modules pool
	 * @var array
	 */
	private $kmodules;


	/**
	 * Collection that stores all information for local manifests and instantiates components
	 * @var InfoCollection
	 */
	private $localPackages;

	/**
	 * Broker that stores all information for remote nodes manifest.rdf
	 * @var array Array of InfoCollection objects
	 */
	private $remotePackages;


	/**
	 * Component broker initialization routine
	 *
	 * @return void
	 */
	public function init() {
		global $sys;

		$this->localPackages = new InfoCollection();
		$this->remotePackages = array();
		/*
		 * Scan every package folder and load its manifest
		 */
		foreach ($sys->config->get('packages.add') as $path) {
			try {
				$this->localPackages->loadManifest($path.'/manifest.rdf');
			} catch (Exception $e) {
				trigger_error('Error loading local package manifest from '.$path.'/manifest.rdf', E_USER_ERROR);
				trigger_error($e->getMessage(), E_USER_ERROR);
			}
		}

		$this->components = array(STATEFUL => array(), STATELESS => array());

	}


	/**
	 * Load class code
	 *
	 * This method should be called by automatic class loader. If required, this method can also get a stub code from naming facility.
	 *
	 * @param string Fully qualified class name including namespace prefix
	 * @return void
	 */
	public function loadClass($className) {
		global $sys;
		try {
			trigger_error(sprintf("Attempting to load class [%s]", $className));
			$res = $this->localPackages->findResourceByKey(ResourceInfo::createKey('mosaic:resource:class', $className));

			if ($res) {
				$res->prepare();
				return;
			} else {
				trigger_error('Trying to load remote stub ...');
				$req = new RemoteObjectStubRequest($className);
				$response = $req->sendReceive($sys->nsSocket);
				$code = $response->getData();
				eval($code);
				return;
			}
			trigger_error('Class '.$className.' is not loadable', E_USER_ERROR);
		} catch (\Exception $e) {
			die('Critical error in class loader: '.$e);

		}
			

	}



	/**
	 * Get a kernel module component instance
	 *
	 * Function returns a component instance associated with kernel module name. Context aware.
	 *
	 * @param string Name of kernel module
	 * @return Component
	 */
	public function getKModuleInstance($name) {
		global $sys;
		
		if (!is_object($sys->context)) {
			trigger_error('No active context!', E_USER_ERROR);
			return;
		}

		if (isset($this->kmodules[$sys->context->__getUri()][$name])) return $this->kmodules[$sys->context->__getUri()][$name];

		$c = $this->getProvidingComponentInstances('mosaic:resource:kmodule', $name);
		$this->kmodules[$sys->context->__getUri()][$name] = $c[0];
		$this->kmodules[$sys->context->__getUri()][$name]->onKModuleInit();
		return $c[0];
	}


	/**
	 * Get a component instance.
	 *
	 * Function returns a component instance  that provides a particular resource. Context aware.
	 * If resource is provided by remote component it removes RemoteComponent stub.
	 *
	 * @param string Resource URI as specified in manifest
	 * @param string Resource name
	 * @return array array of Component instances providing a resource
	 */
	public function getProvidingComponentInstances($resourceType, $p1) {
		$res = $this->localPackages->findResourceByKey(ResourceInfo::createKey($resourceType, $p1));
		$ret = array();

		/*
		 * Search local manifests
		 */
		if ($res) foreach ($res->getProvidingComponents() as $componentInfo) {
			$ret[] = $this->getComponentInstance($componentInfo);
		}

		/*
		 * Search remote manifests. Create a remote component container.
		 */
		foreach ($this->remotePackages as $nodeName => $packageCollection) {
			$res = $packageCollection->findResourceByKey(ResourceInfo::createKey($resourceType, $p1));
			if ($res) foreach ($res->getProvidingComponents() as $componentInfo) {
				$ret[] = new RemoteComponent($componentInfo, $nodeName);
			}

		}

		if (!count($ret)) throw new KernelException('Resource is not provided');
		return $ret;
	}


	/**
	 * Get a component instance.
	 *
	 * Function returns a component instance based on provided ComponentInfo. Context aware.
	 *
	 * @param ComponentInfo
	 * @return Component
	 */
	private function getComponentInstance(ComponentInfo $componentInfo) {
		global $sys;
		trigger_error('Component instance request for '.$componentInfo->systemID);
		
		if ($componentInfo->componentType == STATELESS && isset($this->components[STATELESS][$componentInfo->systemID])) {
			trigger_error('Cache hit for stateless component.');
			return $this->components[STATELESS][$componentInfo->systemID];
		} else if ($componentInfo->componentType == STATEFUL && isset($this->components[STATEFUL][$sys->context->__getUri()][$componentInfo->systemID])) {
			trigger_error('Cache hit for stateful component.');
			return $this->components[STATEFUL][$sys->context->__getUri()][$componentInfo->systemID];
		}

		trigger_error('Cache missed. Constructing instance of '.$componentInfo->systemID);

		if (is_array($componentInfo->requiredResources)) foreach ($componentInfo->requiredResources as $res)  $res->prepare();
		$className = $componentInfo->class;
		$component = new $className($componentInfo);

		if ($componentInfo->componentType == STATELESS) {
			$this->components[STATELESS][$componentInfo->systemID] = $component;
		} else {
			$this->components[STATEFUL][$sys->context->__getUri()][$componentInfo->systemID] = $component;
		}

		return $component;

	}



	/**
	 * Unset components associated with context ID.
	 *
	 * Function is called by node kernel internal subsystems as a part of context releasing operation.
	 *
	 * @param string Context identification URI
	 * @return void
	 */
	public function releaseContext($ctx) {
		/*
		 * "shutdown" compoennts
		 */
		if (@is_array($this->components[STATEFUL][$ctx])) {
			foreach ($this->components[STATEFUL][$ctx] as $component) $component->__stop();
		}

		/*
		 * Unset all known component references
		 */
		if (isset($this->components[STATEFUL][$ctx])) unset($this->components[STATEFUL][$ctx]);
		if (isset($this->kmodules[$ctx])) unset($this->kmodules[$ctx]);
	}


	/**
	 * Loads a remote mananifest from the remote node
	 *
	 * @param string node name
	 * @param string XML RDF manifest of the node
	 * @return void
	 */
	public function loadRemoteManifest($nodeName, $manifest) {
		trigger_error('Loading remote manifest from '.$nodeName);
		$this->remotePackages[$nodeName] = new InfoCollection();
		$this->remotePackages[$nodeName]->loadManifestFromXML($manifest);

	}


	/**
	 * Unloads a remote mananifest from the remote node
	 *
	 * @param string node name
	 * @return void
	 */
	public function unloadRemoteManifest($nodeName) {
		trigger_error('Unloading remote manifest from '.$nodeName);
		unset($this->remotePackages[$nodeName]);
	}

	/**
	 * Private property wrapper to avoid property damage from outside
	 *
	 * @param string Property name
	 * @return mixed
	 */
	function __get($key) {
		if ($key == 'localPackages') {
			return $this->localPackages;
		}
	}
}
